package ee.ut.sysmodel.bgmn.model;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;


public class Session {
	private int turnNumber;
	
	private Player currentPlayer;
	private Player opponent;
	private BoardState board;
	
	private DiceResult[] dice;
	
	public static DiceResult[] rollDice() {
		Random rand = new Random();
		
		int d1 = rand.nextInt(6) + 1;
		int d2 = rand.nextInt(6) + 1;
		
		if(d1 == d2) {
			return new DiceResult[] {new DiceResult(d1), new DiceResult(d1), new DiceResult(d2),
					new DiceResult(d2) };
		} else {
			return new DiceResult[] { new DiceResult(d1), new DiceResult(d2) };
		}
	}
	
	public Session(Player currentPlayer, Player opponent, DiceResult[] dice) {
		this(currentPlayer, opponent, createInitialState(currentPlayer, opponent), dice);
	}
	
	public Session(Player currentPlayer, Player opponent, BoardState board, DiceResult[] dice) {
		this.currentPlayer = currentPlayer;
		this.opponent = opponent;
		
		this.dice = Arrays.copyOf(dice, dice.length);
		
		this.board = board;
		
		turnNumber = 1;
	}
	
	public Player getCurrentPlayer() {
		return currentPlayer;
	}
	public Player getOpponentPlayer(){
		return opponent;
	}
	public BoardState getBoardState() {
		return board;
	}
	
	public int getTurnNumber() {
		return turnNumber;
	}
	
	public void switchTurns() {
		Player oldCurrent = currentPlayer;
		
		currentPlayer = opponent;
		opponent = oldCurrent;
		
		++turnNumber;
		
		dice = rollDice();
	}
	
	public void makeMove(Move move) throws BackgammonException {
		if(!move.canBeDone())
			throw new BackgammonException("Move " + move + " can't be done");
		
		move.getSource().removePiece();
		if(move.getTarget().addPiece(currentPlayer)) {
			opponent.addPieceToBar();
		}
		
		move.useDice();
	}

	private boolean canMoveToPin(Pin targetPin) {
		return !targetPin.isOwned() || targetPin.getOwner().equals(currentPlayer) || targetPin.getPieceCount() == 1;

	}
	private ArrayList<Pin> getCurrentPlaterOccupiedPoints(){
		ArrayList<Pin> pins =  new ArrayList<Pin>();
		for(Pin pin : board.getAllPins()){
			if(pin.getOwner() != null && pin.getOwner().equals(currentPlayer)){
				pins.add(pin);
			}
		}
		
		if(currentPlayer.getBarCount() > 0) {
			int barNr = currentPlayer.getIdentity() == Player.Identity.PLAYER_ONE ? BoardState.P1_SOURCE_BAR :
				BoardState.P2_SOURCE_BAR;
			pins.add(board.getPin(barNr));
		}
		
		return pins;
	}
	
	public DiceResult[] getDiceByValue(int value) {
		int left = value;
		for (DiceResult d : dice) {
			if (d.getResult() == left && !d.isUsed()) {
				return new DiceResult[] { d };
			}
		}
		ArrayList<DiceResult> results = new ArrayList<DiceResult>();
		for (DiceResult d : dice) {
			if (left > 0 && !d.isUsed()) {
				left -= d.getResult();
				results.add(d);
			}
		}
		
		if(left == 0)
			return results.toArray(new DiceResult[results.size()]);
		else
			return new DiceResult[] {};
	}

	/* So...
	 * Problem: need to add move generation for pieces in bars
	 * 
	 * Solution: add bogus pinNr value (BoardState constants). The actual
	 * calculation code depends on Pin.getNumber() method. The class
	 * Bar sets the number to a valid position (e.g, for player2, barred
	 * pieces start from pin #0)
	 * 
	 * This is not a great thing, but at least it can be integrated
	 * into existing code
	 */
	public List<Move> getAvailableMoves(int pinNr){
		if(currentPlayer.getBarCount() > 0) {
			int barNr = currentPlayer.getIdentity() == Player.Identity.PLAYER_ONE ? BoardState.P1_SOURCE_BAR
					: BoardState.P2_SOURCE_BAR;

			if(pinNr != barNr)
				return new ArrayList<Move>();
		}
		
		Pin srcPin = board.getPin(pinNr);
		
		/* Bars and goals are owned despite possibly having 0 pieces */
		if(srcPin.getOwner()==null || srcPin.getPieceCount() == 0 || srcPin.getOwner().getIdentity() !=currentPlayer.getIdentity()){
			return new ArrayList<Move>();
		}
		
		List<Move> moves = getBearOffMoves(srcPin); // Add bearoff if possible
		List<Integer> targets = calculateTargets();
		
		for(Integer i : targets){
			int targetNumber = currentPlayer.getIdentity() == Player.Identity.PLAYER_ONE ? srcPin.getNumber() + i :
				srcPin.getNumber() - i;
			
			if(targetNumber > BoardState.PIN_COUNT || targetNumber < 1)
				continue;
			
			Pin targetPin = getBoardState().getPin(targetNumber);
			
			if(canMoveToPin(targetPin)){
				DiceResult[] dice = getDiceByValue(i);
				
				if(dice.length > 0 && hasFreePath(srcPin.getNumber(), dice))
					moves.add(new Move(srcPin, targetPin, getDiceByValue(i)));
			} 
		}
		
		return moves;
	}
	
	private boolean hasFreePath(int sourceNumber, DiceResult[] dice) {
		int direction = currentPlayer.getIdentity() == Player.Identity.PLAYER_ONE ? 1 : -1;
		
		switch(dice.length) {
		case 1:
			return canMoveToPin(board.getPin(sourceNumber + direction * dice[0].getResult()));
		case 2: {
			int d1r = dice[0].getResult();
			int d2r = dice[1].getResult();

			return (canMoveToPin(board.getPin(sourceNumber + direction * d1r)) || 
					canMoveToPin(board.getPin(sourceNumber + direction * d2r)))
					&& canMoveToPin(board.getPin(sourceNumber + direction * (d1r + d2r)));
		}
		default: {
			int sum = 0;
			for(DiceResult dr : dice) {
				sum += dr.getResult();
				
				if(!canMoveToPin(board.getPin(sourceNumber + direction * sum)))
					return false;
			}
			return true;
		}
		}
	}
	
	public boolean isTurnFinished() {
		boolean diceLeft = false;
		
		for(DiceResult dr : dice) {
			if(!dr.isUsed()) {
				diceLeft = true;
				break;
			}
		}
		
		if(!diceLeft)
			return true;
		
		for(Pin p : getCurrentPlaterOccupiedPoints()) {
			if(getAvailableMoves(p.getNumber()).size() > 0)
				return false;
		}
		
		return true;
	}
	
	private boolean isPinInBearOffRange(int pinNr) {
		if(currentPlayer.getIdentity() == Player.Identity.PLAYER_TWO) {
			return pinNr <= 6 && pinNr >= 1;
		} else {
			return pinNr <= 24 && pinNr >= 18;
		}
	}
	
	private List<Move> getBearOffMoves(Pin pin) {
		List<Move> moves = new ArrayList<Move>();
		
		if(!isPinInBearOffRange(pin.getNumber()))
			return moves;
		
		if(currentPlayer.getBarCount() > 0)
			return moves;
		
		boolean firstPlayer = currentPlayer.getIdentity() == Player.Identity.PLAYER_ONE;
		
		// hackz to avoid code duplication
		int compareSwitcher = firstPlayer ? -1 : 1;
		int firstHomefieldPin = firstPlayer ? 18 : 6;
		
		for(Pin occupiedPin : getCurrentPlaterOccupiedPoints()) {
			int ocn = occupiedPin.getNumber();
			
			/* If there is an occupied pin outside the homefield, can't bear off anything */
			if(!isPinInBearOffRange(ocn))
				return moves;
			
			if(compareSwitcher * ocn > compareSwitcher * pin.getNumber()) { // > for p2, < for p1
				DiceResult[] dice = getDiceByValue(pin.getNumber());
				
				/* If there is something in homefield, but to the left of @pin, then we can
				 * only use exact count on dice
				 */
				if(dice.length > 0) {
					moves.add(new Move(pin, board.getGoal(currentPlayer), dice));
				}
				
				return moves;
			}
		}
		
		/* If there is absolutely nothing to the left, we can use anything 
		 * big enough
		 */
		for(int i = firstHomefieldPin; i <= pin.getNumber(); i++) {
			DiceResult[] dice = getDiceByValue(i);
			
			if(dice != null)
				moves.add(new Move(pin, board.getGoal(currentPlayer), dice));
		}
		
		return moves;
	}
	
	public DiceResult[] getDiceValues() {
		return dice;
	}

	private List<Integer> calculateTargets() {
		List<Integer> targets = new ArrayList<Integer>();
		
		if(dice.length==1){
			targets.add(dice[0].getResult());
		} else if(dice.length==2){
			targets.add(dice[0].getResult());
			targets.add(dice[1].getResult());
			targets.add(dice[0].getResult()+dice[1].getResult());
		} else if(dice.length==3){
			targets.add(dice[0].getResult());
			targets.add(dice[0].getResult()*2);
			targets.add(dice[0].getResult()*3);
		} else {
			targets.add(dice[1].getResult());
			targets.add(dice[1].getResult()*2);
			targets.add(dice[1].getResult()*3);
			targets.add(dice[1].getResult()*4);
		}
		
		return targets;
	}
	
	private static BoardState createInitialState(Player currentPlayer, Player opponent) {
		Player p1, p2;
		
		if(currentPlayer.getIdentity() == Player.Identity.PLAYER_ONE) {
			p1 = currentPlayer;
			p2 = opponent;
		} else {
			p1 = opponent;
			p2 = currentPlayer;
		}
		
		BoardState boardState = new BoardState(currentPlayer, opponent);
		
		boardState.getPin(1).setState(p1, 2);
		boardState.getPin(6).setState(p2, 5);
		boardState.getPin(8).setState(p2, 3);
		boardState.getPin(12).setState(p1, 5);
		boardState.getPin(13).setState(p2, 5);
		boardState.getPin(17).setState(p1, 3);
		boardState.getPin(19).setState(p1, 5);
		boardState.getPin(24).setState(p2, 2);

		
		return boardState;
	}
}
